// Captain Proto -- Protobuf Object-capability RPC
// Copyright (c) 2009 Kenton Varda, Google Inc., and contributors
// http://capnproto.googlecode.com
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of the Captain Proto project nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <stdio.h>
#include <google/protobuf/compiler/plugin.h>
#include <google/protobuf/compiler/code_generator.h>
#include <google/protobuf/io/zero_copy_stream.h>
#include <google/protobuf/io/printer.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/descriptor.pb.h>
#include "annotations.pb.h"

namespace capnproto {
namespace compiler {

using namespace std;
using namespace google::protobuf;
using namespace google::protobuf::compiler;

// ===================================================================
// Stolen from google/protobuf/stubs/strutil.h

inline bool HasSuffixString(const string& str,
                            const string& suffix) {
  return str.size() >= suffix.size() &&
         str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
}

inline string StripSuffixString(const string& str, const string& suffix) {
  if (HasSuffixString(str, suffix)) {
    return str.substr(0, str.size() - suffix.size());
  } else {
    return str;
  }
}

template <typename ITR>
static inline
void SplitStringToIteratorUsing(const string& full,
                                const char* delim,
                                ITR& result) {
  // Optimize the common case where delim is a single character.
  if (delim[0] != '\0' && delim[1] == '\0') {
    char c = delim[0];
    const char* p = full.data();
    const char* end = p + full.size();
    while (p != end) {
      if (*p == c) {
        ++p;
      } else {
        const char* start = p;
        while (++p != end && *p != c);
        *result++ = string(start, p - start);
      }
    }
    return;
  }

  string::size_type begin_index, end_index;
  begin_index = full.find_first_not_of(delim);
  while (begin_index != string::npos) {
    end_index = full.find_first_of(delim, begin_index);
    if (end_index == string::npos) {
      *result++ = full.substr(begin_index);
      return;
    }
    *result++ = full.substr(begin_index, (end_index - begin_index));
    begin_index = full.find_first_not_of(delim, end_index);
  }
}

void SplitStringUsing(const string& full,
                      const char* delim,
                      vector<string>* result) {
  back_insert_iterator< vector<string> > it(*result);
  SplitStringToIteratorUsing(full, delim, it);
}

// ===================================================================
// Stolen from google/protobuf/compiler/java/java_helpers.cc

const char kThickSeparator[] =
  "// ===================================================================\n";
const char kThinSeparator[] =
  "// -------------------------------------------------------------------\n";

const char* kDefaultPackage = "";

const string& FieldName(const FieldDescriptor* field) {
  // Groups are hacky:  The name of the field is just the lower-cased name
  // of the group type.  In Java, though, we would like to retain the original
  // capitalization of the type name.
  if (field->type() == FieldDescriptor::TYPE_GROUP) {
    return field->message_type()->name();
  } else {
    return field->name();
  }
}

string UnderscoresToCamelCaseImpl(const string& input, bool cap_next_letter) {
  string result;
  // Note:  I distrust ctype.h due to locales.
  for (int i = 0; i < input.size(); i++) {
    if ('a' <= input[i] && input[i] <= 'z') {
      if (cap_next_letter) {
        result += input[i] + ('A' - 'a');
      } else {
        result += input[i];
      }
      cap_next_letter = false;
    } else if ('A' <= input[i] && input[i] <= 'Z') {
      if (i == 0 && !cap_next_letter) {
        // Force first letter to lower-case unless explicitly told to
        // capitalize it.
        result += input[i] + ('a' - 'A');
      } else {
        // Capital letters after the first are left as-is.
        result += input[i];
      }
      cap_next_letter = false;
    } else if ('0' <= input[i] && input[i] <= '9') {
      result += input[i];
      cap_next_letter = true;
    } else {
      cap_next_letter = true;
    }
  }
  return result;
}

string UnderscoresToCapitalizedCamelCase(const FieldDescriptor* field) {
  return UnderscoresToCamelCaseImpl(FieldName(field), true);
}

string UnderscoresToCamelCase(const MethodDescriptor* method) {
  return UnderscoresToCamelCaseImpl(method->name(), false);
}

string StripProto(const string& filename) {
  if (HasSuffixString(filename, ".protodevel")) {
    return StripSuffixString(filename, ".protodevel");
  } else {
    return StripSuffixString(filename, ".proto");
  }
}

string FileClassName(const FileDescriptor* file) {
  if (file->options().has_java_outer_classname()) {
    return file->options().java_outer_classname();
  } else {
    string basename;
    string::size_type last_slash = file->name().find_last_of('/');
    if (last_slash == string::npos) {
      basename = file->name();
    } else {
      basename = file->name().substr(last_slash + 1);
    }
    return UnderscoresToCamelCaseImpl(StripProto(basename), true);
  }
}

string FileJavaPackage(const FileDescriptor* file) {
  if (file->options().has_java_package()) {
    return file->options().java_package();
  } else {
    string result = kDefaultPackage;
    if (!file->package().empty()) {
      if (!result.empty()) result += '.';
      result += file->package();
    }
    return result;
  }
}

string ClassName(const FileDescriptor* descriptor) {
  string result = FileJavaPackage(descriptor);
  if (!result.empty()) result += '.';
  result += FileClassName(descriptor);
  return result;
}

string ToJavaName(const string& full_name, const FileDescriptor* file) {
  string result;
  if (file->options().java_multiple_files()) {
    result = FileJavaPackage(file);
  } else {
    result = ClassName(file);
  }
  if (!result.empty()) {
    result += '.';
  }
  if (file->package().empty()) {
    result += full_name;
  } else {
    // Strip the proto package from full_name since we've replaced it with
    // the Java package.
    result += full_name.substr(file->package().size() + 1);
  }
  return result;
}

inline string ClassName(const Descriptor* descriptor) {
  return ToJavaName(descriptor->full_name(), descriptor->file());
}
inline string ClassName(const EnumDescriptor* descriptor) {
  return ToJavaName(descriptor->full_name(), descriptor->file());
}
inline string ClassName(const ServiceDescriptor* descriptor) {
  return ToJavaName(descriptor->full_name(), descriptor->file());
}
inline string ExtensionIdentifierName(const FieldDescriptor* descriptor) {
  return ToJavaName(descriptor->full_name(), descriptor->file());
}

inline bool HasDescriptorMethods(const FileDescriptor* descriptor) {
  return descriptor->options().optimize_for() !=
           FileOptions::LITE_RUNTIME;
}

// ===================================================================
// Helpers

string ToString(int i) {
  char buffer[32];
  sprintf(buffer, "%d", i);
  return buffer;
}

string PackageDirectory(const FileDescriptor* file) {
  string result = FileJavaPackage(file);
  for (int i = 0; i < result.size(); i++) {
    if (result[i] == '.') result[i] = '/';
  }
  if (!result.empty()) result += '/';
  return result;
}

// Name of the file containing the given message type.
string FileNameContaining(const Descriptor* descriptor) {
  if (descriptor->file()->options().java_multiple_files()) {
    while (descriptor->containing_type() != NULL) {
      descriptor = descriptor->containing_type();
    }
    return PackageDirectory(descriptor->file()) + descriptor->name() + ".java";
  } else {
    return PackageDirectory(descriptor->file()) +
           FileClassName(descriptor->file()) + ".java";
  }
}

const ServiceDescriptor* FindService(
    const FileDescriptor* scope, const string& name, string* error) {
  string package = scope->package();
  const ServiceDescriptor* service;

  while (true) {
    string name_to_try;
    if (package.empty()) {
      name_to_try = name;
    } else {
      name_to_try = package + '.' + name;
    }

    service = scope->pool()->FindServiceByName(name_to_try);
    if (service != NULL) break;

    if (package.empty()) {
      *error = "Service type \"" + name + "\" not defined.";
      return NULL;
    }

    string::size_type dotpos = package.find_last_of('.');
    if (dotpos == string::npos) {
      package.clear();
    } else {
      package.erase(dotpos);
    }
  }

  if (service->file() == scope) return service;

  for (int i = 0; i < scope->dependency_count(); i++) {
    if (service->file() == scope->dependency(i)) return service;
  }

  *error = "Service type \"" + service->full_name() + "\" was defined in \"" +
           service->file()->name() + "\", which was not imported by \"" +
           scope->name() + "\".  To use this type, please add the necessary "
           "import.";
  return NULL;
}

// ===================================================================

bool GenerateEmbeddedServiceAccessors(
    const char* template_text, const string& scope,
    const Descriptor* descriptor,
    compiler::OutputDirectory* output_directory, string* error) {
  scoped_ptr<io::ZeroCopyOutputStream> output(
      output_directory->OpenForInsert(
        FileNameContaining(descriptor),
        scope + "_scope:" + descriptor->full_name()));

  io::Printer printer(output.get(), '$');

  for (int i = 0; i < descriptor->field_count(); i++) {
    const FieldDescriptor* field = descriptor->field(i);

    const string& service_type =
        field->options().GetExtension(field_options).service_type();
    if (service_type.empty()) continue;

    switch (field->cpp_type()) {
      case FieldDescriptor::CPPTYPE_INT32:
      case FieldDescriptor::CPPTYPE_INT64:
      case FieldDescriptor::CPPTYPE_UINT32:
      case FieldDescriptor::CPPTYPE_UINT64:
        break;
      default:
        *error = field->full_name() + ": Services must use integer type.";
        return false;
    }

    const ServiceDescriptor* service =
        FindService(descriptor->file(), service_type, error);
    if (service == NULL) {
      *error = field->full_name() + ": " + *error;
      return false;
    }

    printer.Print(
      template_text,
      "name", UnderscoresToCapitalizedCamelCase(field),
      "type", ClassName(service));
  }

  return true;
}

bool GenerateEmbeddedServiceAccessors(
    const Descriptor* descriptor,
    compiler::OutputDirectory* output_directory,
    string* error) {
  for (int i = 0; i < descriptor->nested_type_count(); i++) {
    if (!GenerateEmbeddedServiceAccessors(
          descriptor->nested_type(i), output_directory, error)) {
      return false;
    }
  }

  return GenerateEmbeddedServiceAccessors(
           "$type$ get$name$(capnproto.ServiceReceiver receiver) {\n"
           "  return $type$.fromChannel(receiver.receive(get$name$()));\n"
           "}\n",
           "class", descriptor, output_directory, error) &&
         GenerateEmbeddedServiceAccessors(
           "Builder set$name$($type$ service,\n"
           "                  capnproto.ServiceSender sender) {\n"
           "  return set$name$(sender.send(service.toChannel()));\n"
           "}\n",
           "builder", descriptor, output_directory, error);
}

void GenerateService(
    const ServiceDescriptor* descriptor,
    io::Printer* printer) {
  printer->Indent();

  for (int i = 0; i < descriptor->method_count(); i++) {
    const MethodDescriptor* method = descriptor->method(i);

    map<string, string> vars;
    vars["name"] = UnderscoresToCamelCase(method);
    vars["request"] = ClassName(method->input_type());
    vars["response"] = ClassName(method->output_type());

    printer->Print(vars,
      "public $response$ $name$(\n"
      "    capnproto.RpcContext context,\n"
      "    $request$ request)\n"
      "    throws capnproto.ServiceException {\n"
      "  throw new ServiceException(\"Method not implemented: $name$\");\n"
      "}\n");
  }

  // -----------------------------------------------------------------

  printer->Print(
    "\n"
    "private enum MethodEnum {\n");
  printer->Indent();

  for (int i = 0; i < descriptor->method_count(); i++) {
    printer->Print(
      "$name$,\n",
      "name", UnderscoresToCamelCase(descriptor->method(i)));
  }

  printer->Outdent();
  printer->Print(
    "}\n");

  if (HasDescriptorMethods(descriptor->file())) {
    printer->Print(
      "\n"
      "com.google.protobuf.Descriptors.ServiceDescriptor getDescriptor() {\n"
      "  return $file$.getDescriptor().getServices().get($index$);\n"
      "}\n",
      "file", ClassName(descriptor->file()),
      "index", ToString(descriptor->index()));
  }

  // -----------------------------------------------------------------

  printer->Print(
    "\n"
    "public static $classname$ fromChannel(final capnproto.Channel channel) {\n"
    "  return new $classname$() {\n",
    "classname", descriptor->name());

  printer->Indent();
  printer->Indent();

  for (int i = 0; i < descriptor->method_count(); i++) {
    const MethodDescriptor* method = descriptor->method(i);

    map<string, string> vars;
    vars["name"] = UnderscoresToCamelCase(method);
    vars["request"] = ClassName(method->input_type());
    vars["response"] = ClassName(method->output_type());

    printer->Print(vars,
      "public $response$ $name$(\n"
      "    capnproto.RpcContext context,\n"
      "    $request$ request)\n"
      "    throws capnproto.ServiceException {\n"
      "  return channel.callMethod(\"$name$\", context, request,\n"
      "      $response$.getDefaultInstance());\n"
      "}\n");
  }

  printer->Outdent();
  printer->Outdent();

  printer->Print(
    "  };\n"
    "}\n");

  // -----------------------------------------------------------------

  printer->Print(
    "\n"
    "public final capnproto.Channel toChannel() {\n"
    "  return new capnproto.GeneratedChannel() {\n",
    "classname", descriptor->name());

  printer->Indent();
  printer->Indent();

  printer->Print(
    "@Override\n"
    "public <_ResponseType extends com.google.protobuf.MessageLite>\n"
    "_ResponseType callMethod(\n"
    "    String methodName, capnproto.RpcContext context,\n"
    "    com.google.protobuf.MessageLite request,\n"
    "    _ResponseType responsePrototype)\n"
    "    throws capnproto.ServiceException {\n"
    "  MethodEnum method = MethodEnum.valueOf(methodName);\n"
    "  if (method == null) {\n"
    "    throw new capnproto.ServiceException(\n"
    "        \"Unknown method: \" + methodName);\n"
    "  }\n"
    "  switch (method) {\n");

  printer->Indent();
  printer->Indent();

  for (int i = 0; i < descriptor->method_count(); i++) {
    const MethodDescriptor* method = descriptor->method(i);

    printer->Print(
      "case $name$:\n"
      "  return super.coerceType(\n"
      "      $name$(context, super.coerceType(\n"
      "        request, $request$.getDefaultInstance())),\n"
      "      responsePrototype);\n",
      "name", UnderscoresToCamelCase(method),
      "request", ClassName(method->input_type()));
  }

  printer->Outdent();
  printer->Outdent();

  printer->Print(
    "  }\n"
    "  throw new IllegalStateException(\"Can't get here.\");\n"
    "}\n"
    "\n"
    "@Override\n"
    "public capnproto.GeneratedChannel.PrototypePair getPrototypes(\n"
    "    String methodName) throws capnproto.ServiceException {\n"
    "  MethodEnum method = MethodEnum.valueOf(methodName);\n"
    "  if (method == null) {\n"
    "    throw new capnproto.ServiceException(\n"
    "        \"Unknown method: \" + methodName);\n"
    "  }\n"
    "  switch (method) {\n");

  printer->Indent();
  printer->Indent();

  for (int i = 0; i < descriptor->method_count(); i++) {
    const MethodDescriptor* method = descriptor->method(i);

    map<string, string> vars;
    vars["name"] = UnderscoresToCamelCase(method);
    vars["request"] = ClassName(method->input_type());
    vars["response"] = ClassName(method->output_type());

    printer->Print(vars,
      "case $name$:\n"
      "  return new capnproto.GeneratedChannel.PrototypePair(\n"
      "    $request$.getDefaultInstance(),\n"
      "    $response$.getDefaultInstance());\n");
  }

  printer->Outdent();
  printer->Outdent();

  printer->Print(
    "  }\n"
    "  throw new IllegalStateException(\"Can't get here.\");\n"
    "}\n");

  printer->Outdent();
  printer->Outdent();

  printer->Print(
    "  };\n"
    "}\n");

  // -----------------------------------------------------------------

  printer->Outdent();
}

class JavaGenerator : public compiler::CodeGenerator {
 public:
  // implements CodeGenerator ----------------------------------------
  bool Generate(const FileDescriptor* file,
                const string& parameter,
                compiler::OutputDirectory* output_directory,
                string* error) const {
    if (file->service_count() > 0 && file->options().java_generic_services()) {
      *error = "You must set \"option java_generic_services = false;\" to use "
               "Captain Proto with this file in Java.";
      return false;
    }

    for (int i = 0; i < file->message_type_count(); i++) {
      if (!GenerateEmbeddedServiceAccessors(
            file->message_type(i), output_directory, error)) {
        return false;
      }
    }

    if (file->options().java_multiple_files()) {
      for (int i = 0; i < file->service_count(); i++) {
        scoped_ptr<io::ZeroCopyOutputStream> output(
            output_directory->Open(PackageDirectory(file) +
                                   file->service(i)->name() + ".java"));
        io::Printer printer(output.get(), '$');

        printer.Print(
          "// Generated by the Captain Proto compiler.  DO NOT EDIT!\n"
          "// source: $filename$\n"
          "\n"
          "package $package$;\n",
          "filename", file->name(),
          "package", FileJavaPackage(file));

        printer.Print(
          "\n"
          "public abstract class $classname$ {\n",
          "classname", file->service(i)->name());

        GenerateService(file->service(i), &printer);

        printer.Print(
          "}\n");
      }
    } else {
      scoped_ptr<io::ZeroCopyOutputStream> output(
          output_directory->OpenForInsert(
            PackageDirectory(file) + FileClassName(file) + ".java",
            "outer_class_scope"));
      io::Printer printer(output.get(), '$');

      for (int i = 0; i < file->service_count(); i++) {
        printer.Print(
          "\n"
          "public static abstract class $classname$ {\n",
          "classname", file->service(i)->name());

        GenerateService(file->service(i), &printer);

        printer.Print(
          "}\n");
      }
    }

    return true;
  }
};

}  // namespace compiler
}  // namespace capnproto

int main(int argc, char* argv[]) {
  capnproto::compiler::JavaGenerator generator;
  return google::protobuf::compiler::PluginMain(argc, argv, &generator);
}
